Skip to content

feat: Implement vrf router with auto-allocating evpn_vni attribute#2109

Open
stevekeay wants to merge 2 commits into
mainfrom
vrf-router-evpn-vni
Open

feat: Implement vrf router with auto-allocating evpn_vni attribute#2109
stevekeay wants to merge 2 commits into
mainfrom
vrf-router-evpn-vni

Conversation

@stevekeay

Copy link
Copy Markdown
Contributor

This creates a standalone plugin for 2026.1 to provide the vrf router behaviour we need, including auto-assignment of VNIs from a configured range.

Some of this overlaps work in progress upstream. I have intentionally mirrored the --evpn-vni API surface of the evpn plugin. We are not using any of the actual evpn plugin code but (in theory) once it gains feature parity, we should be able to switch over to it without affecting our users.

This got fairly big. I was thinking it might be cleaner to break it out into a separate package (that we would install separately via pip install in the container) but I wanted to solicit opinions from the team.

To use this:

  • we can roll neutron back to stock 2026.1 (removing the stuff we cherry-picked)
  • change the neutron.conf service_plugins removing evpn and adding understack_vni
  • Add a [understack_vni] section like vni_ranges = 10000:19999



def register_understack_vni_opts(config):
with contextlib.suppress(cfg.DuplicateOptError):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this triggering? This one is a bit concerning to me that we'll get bad data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this should not have been present in the production code - I believe LLM added it to allow it to register different options for various test cases. I have removed it in cbbc38a.

This was added because a test case was registering multiple conflicting
options.  I am removing because it is no longer required.
"allow_post": True,
"allow_put": False,
"convert_to": converters.convert_to_int_if_not_none,
"default": 0,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should that be 0 or ATTR_NOT_SPECIFIED ? I am concerned if having it as 0 will not prevent non-admin users from creating routers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit hazy on the logic for collections, but I think we want non-admin users to create routers, we just don't want them to be able to choose a VNI.

A value of zero is explicitly interpreted as "please auto-assign" in the assignment hook, so the value that actually gets persisted should be non-zero.

Technically it may be more correct to use the sentinel value but I thought this was going to the DB layer, I'm not sure what the best answer is.

@skrobul skrobul Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The python/neutron-understack/neutron_understack/conf/policies/evpn.py has:

   policy.DocumentedRuleDefault(
        name="create_router:evpn_vni",
        check_str=base.ADMIN,
        scope_types=["project"],
        description="Specify ``evpn_vni`` attribute when creating a router",
        operations=ACTION_POST,
...

which will translate to "create_router:evpn_vni": "rule:admin_only" inside a default policy (similarly to stock one and if the user provides any value of the attribute in the request, it will match the rule, effectively requiring admin privileges, so we may need to adjust base.ADMIN to sth else.

neutron-understack = "neutron_understack.conf.policies:list_rules"

[project.entry-points."neutron.db.alembic_migrations"]
neutron-understack = "neutron_understack.db.migration:alembic_migrations"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems to be missing from __init__.py. So it probably needs to have from neutron_understack.db.migration import alembic_migrations or maybe this will work(untested)

Suggested change
neutron-understack = "neutron_understack.db.migration:alembic_migrations"
neutron-understack = "neutron_understack.db.migration.alembic_migrations"

Comment on lines +143 to +158
never_used_vni = self._find_never_used_vni(context, ranges)
if never_used_vni is not None:
allocation = vni_models.UnderstackRouterVNIAllocation(
vni=never_used_vni,
router_id=router_id,
)
context.session.add(allocation)
context.session.flush()
return never_used_vni

released = self._find_released_allocation(context, ranges)
if released:
released.router_id = router_id
released.released_at = None
context.session.flush()
return released.vni

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While slim chances with our workload, there is a risk of concurrent auto allocations picking the same never-used VNI. The faster one gets it, the loser will get 500 error. Consider catching a duplicate violation and retrying for more graceful handling:

Suggested change
never_used_vni = self._find_never_used_vni(context, ranges)
if never_used_vni is not None:
allocation = vni_models.UnderstackRouterVNIAllocation(
vni=never_used_vni,
router_id=router_id,
)
context.session.add(allocation)
context.session.flush()
return never_used_vni
released = self._find_released_allocation(context, ranges)
if released:
released.router_id = router_id
released.released_at = None
context.session.flush()
return released.vni
for _attempt in range(3):
never_used_vni = self._find_never_used_vni(context, ranges)
if never_used_vni is not None:
try:
allocation = vni_models.UnderstackRouterVNIAllocation(
vni=never_used_vni,
router_id=router_id,
)
context.session.add(allocation)
context.session.flush()
return never_used_vni
except db_exc.DBDuplicateEntry:
context.session.rollback()
continue # another worker won; recompute candidate
released = self._find_released_allocation(context, ranges)
if released:
released.router_id = router_id
released.released_at = None
context.session.flush()
return released.vni
raise UnderstackVNINoAvailable(ranges=format_vni_ranges(ranges))
raise UnderstackVNINoAvailable(ranges=format_vni_ranges(ranges))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also oslo_db_api.wrap_db_retry decorator which may work here

@oslo_db_api.wrap_db_retry(
    max_retries=3,
    retry_on_deadlock=True,
    exception_checker=lambda exc: isinstance(exc, db_exc.DBDuplicateEntry),
)


raise UnderstackVNINoAvailable(ranges=format_vni_ranges(ranges))

def _allocate_specific_vni(self, context, router_id, vni):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly to the above, it might be worth wrapping the context.session.flush() with catch for duplicates, although the retry would not make any sense here

 try:
        context.session.flush()
    except db_exc.DBDuplicateEntry:
        context.session.rollback()
        raise UnderstackVNIInUse(vni=vni, router_id="unknown")

or something like that


MIN_VNI = getattr(n_const, "MIN_VXLAN_VNI", 1)
MAX_VNI = n_const.MAX_VXLAN_VNI
DEFAULT_VNI_RANGES = [f"{MIN_VNI}:{MAX_VNI}"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants